package org.pinda.network.util;

/**
 * A byte buffer which will manage larger packets on a stream
 * @author Johnnei
 *
 */
public class Stream {
	
	/**
	 * The actual buffer of bytes
	 */
	private byte[] buffer;
	/**
	 * The index of the next byte to read
	 */
	private int readOffset;
	/**
	 * The index of the next spot to write
	 */
	private int writeOffset;
	
	/**
	 * Creates a new byte buffer of the given size
	 * @param size The initial size of the stream
	 */
	public Stream(int size) {
		buffer = new byte[size];
	}
	
	/**
	 * Adds a single byte to the buffer
	 * @param b the byte to add
	 */
	public void addToBuffer(byte b) {
		addToBuffer(new byte[] { b });
	}
	
	public void addToBuffer(byte[] b) {
		//Ensure Capacity
		if(buffer.length - writeOffset <= b.length) {
			int extraNeeded = b.length - getWriteAvailable();
			//Can we shift the buffer in such a way that we don't need to resize?
			if(readOffset >= extraNeeded) {
				//Rebase the readOffset to 0, this creates more space on the end
				System.arraycopy(buffer, readOffset, buffer, 0, buffer.length - readOffset);
				writeOffset -= readOffset;
				readOffset = 0;
			} else {
				//Calculate the extra space which we will allocate
				int extraSize = Math.max(buffer.length / 10, 10); //Try a 10th of the actual size, or if its less than 10 bytes just add 10
				while(extraSize < extraNeeded - readOffset) { //We'll also rebase the stream
					extraSize *= 2;
				}
				//Increase the buffer with the calculated extra
				byte[] newBuffer = new byte[buffer.length + extraSize];
				System.arraycopy(buffer, readOffset, newBuffer, 0, writeOffset - readOffset);
				buffer = newBuffer;
			}
		}
		//Write to buffer
		System.arraycopy(b, writeOffset, b, 0, b.length);
		writeOffset += b.length;
	}
	
	/**
	 * The amount of writable space we can have.
	 * @return The amount of writable bytes
	 */
	public int getWriteAvailable() {
		return buffer.length - writeOffset;
	}
	
	/**
	 * Reads a byte from the buffer
	 * @return the byte
	 */
	private int read() {
		return buffer[readOffset++];
	}
	
	/**
	 * Reads a single byte
	 * @return a byte
	 */
	public int readByte() {
		return read();
	}
	
	public byte[] readBytes(int count) {
		byte[] bytes = new byte[count];
		for(int i = 0; i < count; i++) {
			bytes[i] = (byte)read();
		}
		return bytes;
	}
	
	/**
	 * Reads two bytes into a short
	 * @return a short
	 */
	public int readShort() {
		return (read() << 8) + read();
	}
	
	/**
	 * Reads four bytes into an int
	 * @return an int
	 */
	public int readInt() {
		return (readShort() << 16) + readShort();
	}
	
	/**
	 * Reads eight bytes into a long
	 * @return a long
	 */
	public int readLong() {
		return (readInt() << 32) + readInt();
	}
	
	/**
	 * Reads a byte into a boolean
	 * @return a boolean
	 */
	public boolean readBoolean() {
		return read() == 1;
	}
	
	/**
	 * Peeks up to 4 bytes into the stream
	 * @param bytes the amount of bytes to peek
	 * @return an int
	 */
	public int peek(int bytes) {
		int value = 0;
		int peekOffset = readOffset;
		while(bytes-- > 0) {
			value += (buffer[peekOffset++] << (bytes * 8));
		}
		return value;
	}
	
	/**
	 * Peeks a long from the stream
	 * @return a long
	 */
	public long peekLong() {
		long l = readLong();
		readOffset -= 8;
		return l;
	}

	/**
	 * Checks if the buffer has been filled
	 * @return true if the buffer is full else false
	 */
	public boolean isFull() {
		return writeOffset >= buffer.length;
	}
	
	public byte[] getRawBuffer() {
		return buffer;
	}
	
	public int getWriteOffset() {
		return writeOffset;
	}

	public void addToWriteOffset(int bytesRead) {
		writeOffset += bytesRead;
	}

}
